package org.mspring.platform.api.sitemap;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;

import org.xml.sax.SAXException;

/**
 * Builds a sitemap index, which points only to other sitemaps.
 * 
 * @author Dan Fabulich
 * 
 */
public class SitemapIndexGenerator {
    private final URL baseUrl;
    private final String baseUrlString;
    private final File outFile;
    private final ArrayList<SitemapIndexUrl> urls = new ArrayList<SitemapIndexUrl>();
    private final int maxUrls;
    private final W3CDateFormat dateFormat;
    private final Date defaultLastMod;
    private final boolean autoValidate;
    /** Maximum 1,000 sitemaps per index allowed */
    public static final int MAX_SITEMAPS_PER_INDEX = 1000;

    /** Options to configure sitemap index generation */
    public static class Options {
        private URL baseUrl;
        private File outFile;
        private W3CDateFormat dateFormat = null;
        private int maxUrls = MAX_SITEMAPS_PER_INDEX;
        private Date defaultLastMod = new Date();
        private boolean autoValidate = false;

        // TODO GZIP? Is that legal for a sitemap index?

        /**
         * Configures the generator with a base URL and destination to write the
         * sitemap index file.
         * 
         * @param baseUrl
         *            All URLs in the generated sitemap(s) should appear under
         *            this base URL
         * @param outFile
         *            The sitemap index will be written out at this location
         */
        public Options(URL baseUrl, File outFile) {
            this.baseUrl = baseUrl;
            this.outFile = outFile;
        }

        /**
         * Configures the generator with a base URL and destination to write the
         * sitemap index file.
         * 
         * @param baseUrl
         *            All URLs in the generated sitemap(s) should appear under
         *            this base URL
         * @param outFile
         *            The sitemap index will be written out at this location
         */
        public Options(String baseUrl, File outFile) throws MalformedURLException {
            this(new URL(baseUrl), outFile);
        }

        /**
         * The date formatter, typically configured with a
         * {@link W3CDateFormat.Pattern} and/or a time zone
         */
        public Options dateFormat(W3CDateFormat dateFormat) {
            this.dateFormat = dateFormat;
            return this;
        }

        /**
         * The maximum number of sitemaps to allow per sitemap index; the
         * default is the maximum allowed (1,000), but you can decrease it if
         * you wish (for testing)
         */
        Options maxUrls(int maxUrls) {
            if (maxUrls > MAX_SITEMAPS_PER_INDEX) {
                throw new RuntimeException("You can't have more than 1000 sitemaps per index");
            }
            this.maxUrls = maxUrls;
            return this;
        }

        /**
         * The default lastMod date for sitemap indexes; the default default is
         * now, but you can pass in null to omit a lastMod entirely. We don't
         * recommend this; Google may not like you as much.
         */
        public Options defaultLastMod(Date defaultLastMod) {
            this.defaultLastMod = defaultLastMod;
            return this;
        }

        /**
         * Validate the sitemap index automatically after writing it; this takes
         * time
         */
        public Options autoValidate(boolean autoValidate) {
            this.autoValidate = autoValidate;
            return this;
        }

        /**
         * Constructs a sitemap index generator configured with the options you
         * specified
         */
        public SitemapIndexGenerator build() {
            return new SitemapIndexGenerator(this);
        }
    }

    /**
     * Configures the generator with a base URL and destination to write the
     * sitemap index file.
     * 
     * @param baseUrl
     *            All URLs in the generated sitemap(s) should appear under this
     *            base URL
     * @param outFile
     *            The sitemap index will be written out at this location
     */
    public SitemapIndexGenerator(URL baseUrl, File outFile) {
        this(new Options(baseUrl, outFile));
    }

    /**
     * Configures the generator with a base URL and destination to write the
     * sitemap index file.
     * 
     * @param baseUrl
     *            All URLs in the generated sitemap(s) should appear under this
     *            base URL
     * @param outFile
     *            The sitemap index will be written out at this location
     */
    public SitemapIndexGenerator(String baseUrl, File outFile) throws MalformedURLException {
        this(new Options(baseUrl, outFile));
    }

    private SitemapIndexGenerator(Options options) {
        this.baseUrl = options.baseUrl;
        this.baseUrlString = baseUrl.toString();
        this.outFile = options.outFile;
        this.maxUrls = options.maxUrls;
        W3CDateFormat dateFormat = options.dateFormat;
        if (dateFormat == null)
            dateFormat = new W3CDateFormat();
        this.dateFormat = dateFormat;
        this.defaultLastMod = options.defaultLastMod;
        this.autoValidate = options.autoValidate;
    }

    /** Adds a single sitemap to the index */
    public SitemapIndexGenerator addUrl(SitemapIndexUrl url) {
        UrlUtils.checkUrl(url.url.toString(), baseUrlString);
        if (urls.size() >= maxUrls) {
            throw new RuntimeException("More than " + maxUrls + " urls");
        }
        urls.add(url);
        return this;
    }

    /** Add multiple sitemaps to the index */
    public SitemapIndexGenerator addUrls(Iterable<? extends SitemapIndexUrl> urls) {
        for (SitemapIndexUrl url : urls)
            addUrl(url);
        return this;
    }

    /** Add multiple sitemaps to the index */
    public SitemapIndexGenerator addUrls(SitemapIndexUrl... urls) {
        for (SitemapIndexUrl url : urls)
            addUrl(url);
        return this;
    }

    /** Add multiple sitemaps to the index */
    public SitemapIndexGenerator addUrls(String... urls) throws MalformedURLException {
        for (String url : urls)
            addUrl(url);
        return this;
    }

    /** Adds a single sitemap to the index */
    public SitemapIndexGenerator addUrl(String url) throws MalformedURLException {
        return addUrl(new SitemapIndexUrl(url));
    }

    /** Add multiple sitemaps to the index */
    public SitemapIndexGenerator addUrls(URL... urls) {
        for (URL url : urls)
            addUrl(url);
        return this;
    }

    /** Adds a single sitemap to the index */
    public SitemapIndexGenerator addUrl(URL url) {
        return addUrl(new SitemapIndexUrl(url));
    }

    /** Adds a single sitemap to the index */
    public SitemapIndexGenerator addUrl(URL url, Date lastMod) {
        return addUrl(new SitemapIndexUrl(url, lastMod));
    }

    /** Adds a single sitemap to the index */
    public SitemapIndexGenerator addUrl(String url, Date lastMod) throws MalformedURLException {
        return addUrl(new SitemapIndexUrl(url, lastMod));
    }

    /**
     * Add a numbered list of sitemaps to the index, e.g. "sitemap1.xml"
     * "sitemap2.xml" "sitemap3.xml" etc.
     * 
     * @param prefix
     *            the first part of the filename e.g. "sitemap"
     * @param suffix
     *            the last part of the filename e.g. ".xml" or ".xml.gz"
     * @param count
     *            the number of sitemaps (1-based)
     */
    public SitemapIndexGenerator addUrls(String prefix, String suffix, int count) {
        for (int i = 1; i <= count; i++) {
            String fileName = prefix + i + suffix;
            try {
                addUrl(new URL(baseUrl, fileName));
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            }
        }
        return this;
    }

    /** Writes out the sitemap index */
    public void write() {
        if (urls.size() == 0)
            throw new RuntimeException("No URLs added, sitemap index would be empty; you must add some URLs with addUrls");
        try {
            // TODO gzip? is that legal for a sitemap index?
            FileWriter out = new FileWriter(outFile);
            writeSiteMap(out);
            if (autoValidate)
                SitemapValidator.validateSitemapIndex(outFile);
        } catch (IOException e) {
            throw new RuntimeException("Problem writing sitemap index file " + outFile, e);
        } catch (SAXException e) {
            throw new RuntimeException("Problem validating sitemap index file (bug?)", e);
        }
    }

    private void writeSiteMap(OutputStreamWriter out) throws IOException {
        out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        out.write("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
        for (SitemapIndexUrl url : urls) {
            out.write("  <sitemap>\n");
            out.write("    <loc>");
            out.write(url.url.toString());
            out.write("</loc>\n");
            Date lastMod = url.lastMod;

            if (lastMod == null)
                lastMod = defaultLastMod;

            if (lastMod != null) {
                out.write("    <lastmod>");
                out.write(dateFormat.format(lastMod));
                out.write("</lastmod>\n");
            }
            out.write("  </sitemap>\n");
        }
        out.write("</sitemapindex>");
        out.close();
    }

}
